"use strict";

const fs = require("fs");

var metadata = function(umlModel, context) {
    this.model = umlModel
    this.context = context;
    this.types = new Set();
    this.relationships = new Set();
    this.packages = new Map();
    this.firstTypes = [];
    this.load();
}
metadata.prototype = {
    load: function() {
        var self = this;
        self._scan(self.model);
        self.firstTypes.forEach(function(t){self._resolve(t)});
        self._group();
    },
    _scan: function(ele){
        var self = this;
        if (ele instanceof type.UMLPackage || ele instanceof type.UMLModel) {
            ele.ownedElements.forEach(function(sub){
                self._scan(sub)
            });
        } else if (ele instanceof type.UMLClass || ele instanceof type.UMLInterface || ele instanceof type.UMLEnumeration) {
            self.firstTypes.push(ele);
        }
    },
    _resolve: function(ele) {
        var self = this;
        if (!self.types.has(ele)) {
            if (ele instanceof type.UMLClass || ele instanceof type.UMLInterface) {
                self.types.add(ele);
                ele.getChildNodes().forEach(function(node){self._resolve(node)});
                // ele.getAssociationEnds().forEach(function(end){self._resolve(end._parent)})
            } else if (ele instanceof type.UMLEnumeration) {
                self.types.add(ele);
            } else if (ele instanceof type.UMLAttribute) {
                self._resolve(ele.type);
            } else if (ele instanceof type.UMLGeneralization) {
                self.relationships.add(ele);
                self._resolve(ele.target);
            } else if (ele instanceof type.UMLInterfaceRealization) {
                self.relationships.add(ele);
                self._resolve(ele.target);
            } else if (ele instanceof type.UMLAssociation) {
                self.relationships.add(ele);
                self._resolve(ele.end1.reference);
                self._resolve(ele.end2.reference);
            }
        }
    },
    getTypeName: function(ele, _package) {
        // console.log('type1', ele, _package);
        if (ele instanceof type.UMLDataType) {
            return ele.name;
        }
        if (ele._parent == _package) {
            return ele.name;
        }
        var name = ele._parent.name + "." + ele.name;
        if(ele._parent._parent) {
            name = ele._parent._parent.name + "." + name;
        } else {
            name = this.context.projectName + "." + name;
        }
        return name;
    },
    getAttrTypeName: function(attr) {
        // console.log('type2', attr);
        var datatype = attr.type;
        // console.log('typetype',datatype,type.UMLDataType)
        if (datatype instanceof type.UMLDataType) {
            return datatype.name;
        }
        return this.getTypeName(datatype, attr._parent._parent);
    },
    _group: function() {
        var self = this;
        self.types.forEach(function(t){
            if(!self.packages.has(t._parent)) {
                self.packages.set(t._parent, []);
            }
            self.packages.get(t._parent).push(t);
        });
    },
    getTitle: function(ele, ele2) {
        var doc = ele.documentation;
        if (!doc && ele2) {
            doc = ele2.documentation;
        }
        if (! doc) {
            return "";
        }
        var i = doc.indexOf('\n');
        if (i >= 0) {
            return doc.substring(0, i);
        } else {
            return doc;
        }
    },
    getModuleName: function(k) {
        if(k._parent instanceof type.UMLPackage) {
            return k._parent.name;
        } else {
            return this.context.projectName;
        }
    },
    getTagValue: function(tag) {
        var v = '';
        switch(tag.kind) {
            case 'number':
                v = tag.number;
                break;
            case 'string':
                v = tag.value;
                break;
            case 'boolean':
                v = tag.checked ? 'true' : 'false';
                break;
        }
        return v;
    },
    getAttrTag: function(attr){     
        var self = this;   
        var ex = ''
        if (attr.defaultValue) {
            ex += 'defaultValue="'+attr.defaultValue+'" ';
        }
        if (attr.isID) {
            ex += 'isKey="true" ';
        }
        if (attr.isUnique) {
            ex += 'isUnique="true" ';
        }
        if (attr.tags.length > 0) {
            attr.tags.forEach(function(tag){
                ex += tag.name+ '="'+self.getTagValue(tag)+'" ';
            });
        }
        return ex;
    },
    export: function(printer) {
        var self = this;
        printer.init(self.context);
        var generalizations = new Set();
        var realizations = new Set();
        var aggregations = new Set();
        printer.print(0, '<?xml version="1.0" encoding="utf-8"?>');
        printer.print(0, '<components xmlns="http://www.imeta.org/meta" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imeta.org/meta http://www.imeta.org/meta/meta.xsd">');
        self.packages.forEach(function(v,k){
            printer.print(1, '<!-- %s %s -->', k.name, self.getTitle(k));
            printer.print(1, '<component name="%s" title="%s" moduleName="%s" domain="">', k.name, self.getTitle(k), self.getModuleName(k));
            generalizations.clear();
            realizations.clear();
            aggregations.clear();
            // enum
            v.forEach(function(t){
                // type and property
                if (t instanceof type.UMLEnumeration) {
                    printer.print(2, '<enum name="%s" title="%s" m1Type="Short">', t.name, self.getTitle(t));
                    var index = 0;
                    t.literals.forEach(function(literal) {
                        printer.print(3, '<literal name="%s" title="%s" value="%s" />', literal.name, self.getTitle(literal), index++);
                    })
                    printer.print(2, '</enum>');
                }
            });
            // interface
            v.forEach(function(t){
                // type and property
                if (t instanceof type.UMLInterface) {
                    printer.print(2, '<interface name="%s" title="%s">', t.name, self.getTitle(t));
                    printer.print(3, '<properties>');
                    t.attributes.forEach(function(attr) {
                        printer.print(4, '<property name="%s" title="%s" type="%s" %s/>', attr.name, self.getTitle(attr, attr.type), self.getAttrTypeName(attr), self.getAttrTag(attr));
                    })
                    printer.print(3, '</properties>');
                    printer.print(2, '</interface>');
                }
            });
            // class
            var entityArr = [];
            var entityMap = new Map();
            v.forEach(function(t) {
                if (t instanceof type.UMLClass) {
                    entityArr.push(t.name);
                    entityMap.set(t.name, t);
                }
            });
            entityArr.sort();
            entityArr.forEach(function(sk) {
                // type and property
                var t = entityMap.get(sk);
                printer.print(2, '<class name="%s" title="%s" tableName="" service="">', t.name, self.getTitle(t));
                printer.print(3, '<properties>');
                //aggregation: composition
                t.getAssociationEnds().forEach(function(end) {
                    if (end.aggregation != 'composite') {
                        var target = end._parent.end1 == end ? end._parent.end2 : end._parent.end1;
                        if(target.navigable) {
                            printer.print(4, '<property name="%s" title="%s" type="%s" isRequired="%s" />', target.name, self.getTitle(target, target.reference), self.getTypeName(target.reference, t._parent), target.multiplicity=='1'?"true":"false");
                        }
                    }
                });
                t.attributes.forEach(function(attr) {
                    printer.print(4, '<property name="%s" title="%s" type="%s" %s/>', attr.name, self.getTitle(attr, attr.type), self.getAttrTypeName(attr),self.getAttrTag(attr));
                })
                printer.print(3, '</properties>');
                printer.print(2, '</class>');
            });
            // relationship
            v.forEach(function(t){
                // relationship
                t.getChildNodes().forEach(function(ele){
                    if (ele instanceof type.UMLGeneralization) {
                        generalizations.add(ele);
                    } else if (ele instanceof type.UMLInterfaceRealization) {
                        realizations.add(ele);
                    } else if (ele instanceof type.UMLAssociation) {
                        if(ele.end1.aggregation == 'composition' || ele.end2.aggregation == 'composition') {
                            aggregations.add(ele);
                        }
                    }
                });
            });
            if (generalizations.size>0 ){
                printer.print(2, '<generalizations>');
                generalizations.forEach(function(g){
                    printer.print(3, '<generalization parent="%s" child="%s" />', self.getTypeName(g.target, g.source._parent), g.source.name);
                });
                printer.print(2, '</generalizations>');
            }
            if (realizations.size>0 ){
                printer.print(2, '<realizations>');
                realizations.forEach(function(r){
                    printer.print(3, '<realization supplier="%s" client="%s" />', self.getTypeName(r.target, r.source._parent), r.source.name);
                });
                printer.print(2, '</realizations>');
            }
            if (aggregations.size>0 ){
                printer.print(2, '<aggregations>');
                aggregations.forEach(function(asso){
                    // ZeroToOne,OneToMany,ZeroToMany
                    var aggrEnd = asso.end1.aggregation == 'composition' ? asso.end1 : asso.end2;
                    var childEnd = asso.end1.aggregation == 'composition' ? asso.end2 : asso.end1;
                    var m = 'ZeroToMany';
                    if (childEnd.multiplicity == '1..*') {
                        m = 'OneToMany';
                    } else if (childEnd.multiplicity == '0..1') {
                        m = 'ZeroToOne';
                    }
                    printer.print(3, '<aggregation type="composition" parentRole="%s" aggrParent="%s" childRole="%s" aggrChild="%s" childRoleMulti="%s" />', aggrEnd.name,aggrEnd.reference.name,childEnd.name,childEnd.reference.name,m);
                });
                printer.print(2, '</aggregations>');
            }
            printer.print(1, '</component>');
        });
        printer.print(0, '</components>');
        printer.flush(self.context);
    }
}

var printers = {
    build: function(indentstr, padding, pattern) {
        var arr = [];
        if(padding>0){
            for(var i=0;i<padding;i++){
                arr.push(indentstr)
            }
        }
        if(arguments.length > 3) {
            var arr2 = pattern.split('%s');
            if(arr2.length > 1){
                for(var i=0;i<arr2.length;i++){
                    arr.push(arr2[i]);
                    arr.push(arguments[i+3]);
                }
            } else {
                arr.push(pattern);
            }
        } else {
            arr.push(pattern);
        }
        if (arr.length == 1) {
            return pattern;
        } else {
            return arr.join('');
        }
    },
    indent: function(len) {
        var str = '';
        for(var i=0;i<len;i++){
            str += ' ';
        }
        return str;
    },
    console: (function(){
        var lines = [];
        var context;
        var indentstr = '';
        return {
            init: function(_context) {
                lines = [];
                context = _context;
                indentstr = printers.indent(context.indents);
            },
            print: function(padding, pattern) {
                var args = Array.prototype.slice.call(arguments);
                args.unshift(indentstr);
                var str = printers.build.apply(this, args);
                lines.push(str);
                if (context.debug) {
                    console.log(str, args);
                }
            },
            flush: function (context) {
                console.log(lines.join('\n'));
            }
        };
    })(),
    file: (function(){
        var lines = [];
        var context;
        var indentstr = '';
        return {
            init: function(_context) {
                lines = [];
                context = _context;
                indentstr = printers.indent(context.indents);
            },
            print: function(padding, pattern){
                var args = Array.prototype.slice.call(arguments);
                args.unshift(indentstr);
                var str = printers.build.apply(this, args);
                lines.push(str);
                if (context.debug) {
                    console.log(str, args);
                }
            },
            flush: function (context) {
                fs.writeFileSync(context.path, lines.join('\n'), 'utf8');
            }
        };
    })()
}

var detect = function(context) {
    this.context = context;
};
detect.aliasMap = (function(){
    var map = new Map();
    map.set('int', 'Integer');
    map.set('bool', 'Boolean');
    map.set('boolean', 'Boolean');
    map.set('long', 'Long');
    map.set('byte', 'Byte');
    map.set('short', 'Short');
    map.set('decimal', 'Decimal');
    map.set('string', 'String');
    map.set('date', 'Date');
    map.set('datetime', 'DateTime');
    return map;
})();
detect.prototype = {
    fullpath: function(ele) {
        var arr = [];
        while(ele) {
            arr.unshift(ele.name);
            ele = ele._parent;
        }
        return arr.join("/");
    },
    detectDataType: function(){
        var self = this;
        var primitiveMap = new Map();
        self.context.primitivePackage.ownedElements.filter(function(ele){return ele instanceof type.UMLDataType}).forEach(function(ele){
            primitiveMap.set(ele.name, ele);
        });
        var stat = {total:0,fixed:0,failed:0,error:0};
        self.context.attributes.forEach(function(attr){
            if(!attr.type) {
                stat.error+=1;
                console.error("[detect]", self.fullpath(attr), "The datatype is not assigned");
            } else if (typeof(attr.type)=='string') {
                stat.total+=1;
                if(primitiveMap.has(attr.type)) {
                    stat.fixed+=1;
                    attr.type = primitiveMap.get(attr.type);
                    console.info("[detect]", self.fullpath(attr), attr.type.name, "The datatype is reassigned");
                } else if(detect.aliasMap.has(attr.type)){
                    var name1 = attr.type;
                    var name2 = detect.aliasMap.get(name1);
                    stat.fixed+=1;
                    attr.type = primitiveMap.get(name2);
                    console.warn("[detect]", self.fullpath(attr), name1 + " -> " + name2);
                } else {
                    stat.failed+=1;
                    console.warn("[detect]", self.fullpath(attr), attr.type, "The datatype is not found");
                }
            }
        });
        var msg = ''
        if (stat.failed > 0 || stat.error > 0) {
            msg = "基本类型检测完成，需要修正属性"+stat.total+"个，已修正"+stat.fixed+"个，未修正"+stat.failed+"个，未设置类型"+stat.error+"个，在控制台查看详情";
        } else if(stat.total == 0) {
            msg = "基本类型检测完成，所有属性无需修正";
        } else {
            msg = "基本类型检测完成，需要修正属性"+stat.total+"个，全部修正完成";
        }
        return msg;
    }
}

exports.printers = printers;
exports.metadata = metadata;
exports.detect = detect;